//
//  LocalSerialIOPackTransport.m
//  RunSketch
//
//  Created by Brian on 9/5/09.
//  Copyright 2009 Tellart. All rights reserved.
//

#import "LocalSerialIOPackTransport.h"
#import "SketchProtocol.h"

#define cStrDefaultSerialPort "/dev/tty.iap"

#define LocalSerialIOPackTransport_EX_NOT_SET_UP                @"LocalSerialIOPackTransport: call connectToDev before using any other methods of this object"
#define LocalSerialIOPackTransport_EX_MALLOC_FAILED				@"LocalSerialIOPackTransport: malloc() failed"
#define LocalSerialIOPackTransport_EX_READ_FAILED               @"LocalSerialIOPackTransport: read() failed"
#define LocalSerialIOPackTransport_EX_NOT_CONNECTED             @"LocalSerialIOPackTransport: port is not open"
#define LocalSerialIOPackTransport_EX_WRITE_FAILED              @"LocalSerialIOPackTransport: write() failed"
#define LocalSerialIOPackTransport_EX_WRITE_FAILED_F            @"LocalSerialIOPackTransport: write() failed: %s"

#define LocalSerialIOPackTransport_DEFAULT_READ_BUFFER_SIZE     4096

#define DEV_INVALID_DESCRIPTOR -1
#define FAIL -1

@implementation LocalSerialIOPackTransport

@synthesize deviceAddress, ioPack;

-(id)init
{
    if (self = [super init])
    {
        deviceAddress = nil;
        readBufferSize = LocalSerialIOPackTransport_DEFAULT_READ_BUFFER_SIZE;
        
        [self allocReadBuffer];
        hasBeenSetUp = NO;
        
        self.ioPack = [[IOPack alloc] initWithTransport:self];
    }
    return self;
}

/*
 * much of this method is derived from the tutorial at http://devdot.wikispaces.com/Iphone+Serial+Port+Tutorial
 *
 */
- (void)connectToDev:(NSString*)devPath
{
    if (hasBeenSetUp) [self closePort];

    if ([self openPort])
    {
        deviceAddress = [NSString stringWithFormat:@"@%://%@", kDeviceAddressSchemeLocal, @cStrDefaultSerialPort];
        [deviceAddress retain];
        //done!
        NSLog(@"[LocalSerialIOPackTransport connectToDev]: Port opened!\n", (int) cfgetospeed(&options));
        
        hasBeenSetUp = YES;
        
    } else {
        [self deallocReadBuffer];
    }
        
} //connectToDev

- (BOOL) openPort
{
    NSLog(@"[LocalSerialIOPackTransport openPort]");
    
    NSLog(@"[LocalSerialIOPackTransport openPort] Opening serial port");
    fileDescriptor = open(cStrDefaultSerialPort, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fileDescriptor == DEV_INVALID_DESCRIPTOR)
    {
        NSLog(@"[LocalSerialIOPackTransport openPort]: Error opening serial port %s - %s(%d).\n",
               @cStrDefaultSerialPort, strerror(errno), errno);
        [self closePort];
        return NO;
    }

    /*
    if (ioctl(fileDescriptor, TIOCEXCL) == FAIL)
    {
        NSLog(@"[LocalSerialIOPackTransport openPort]: Error setting TIOCEXCL on %s - %s(%d).\n",
            @cStrDefaultSerialPort, strerror(errno), errno);
        [self closePort];
        return NO;
    }
    */
    
    if (fcntl(fileDescriptor, F_SETFL, 0) == FAIL)
    {
        NSLog(@"[LocalSerialIOPackTransport openPort]: Error clearing O_NONBLOCK %s - %s(%d).\n",
            @cStrDefaultSerialPort, strerror(errno), errno);
        [self closePort];
        return NO;
    }

    if (tcgetattr(fileDescriptor, &oldOptions) == FAIL)
    {
        NSLog(@"[LocalSerialIOPackTransport openPort]: Error getting tty attributes %s - %s(%d).\n",
            @cStrDefaultSerialPort, strerror(errno), errno);
        [self closePort];
        return NO;
    }

    // The serial port attributes such as timeouts and baud rate are set by modifying the termios
    // structure and then calling tcsetattr() to cause the changes to take effect. Note that the
    // changes will not become effective without the tcsetattr() call.
    // See tcsetattr(4) ("man 4 tcsetattr") for details.
    options = oldOptions;
    
    // Print the current input and output baud rates.
    // See tcsetattr(4) ("man 4 tcsetattr") for details.
    NSLog(@"[LocalSerialIOPackTransport openPort]: Current input baud rate is %d\n", (int) cfgetispeed(&options));
    NSLog(@"[LocalSerialIOPackTransport openPort]: Current output baud rate is %d\n", (int) cfgetospeed(&options));

    // Set raw input (non-canonical) mode, with reads blocking until either a single character
    // has been received or a one second timeout expires.
    // See tcsetattr(4) ("man 4 tcsetattr") and termios(4) ("man 4 termios") for details.
    cfmakeraw(&options);
    options.c_cc[VMIN] = 1;
    options.c_cc[VTIME] = 10;
 
    // The baud rate, word length, and handshake options can be set as follows:
    cfsetspeed(&options, B19200);    // Set 19200 baud
    options.c_cflag |= (CS8);  // RTS flow control of input
 
    NSLog(@"[LocalSerialIOPackTransport openPort]: Input baud rate changed to %d\n", (int) cfgetispeed(&options));
    NSLog(@"[LocalSerialIOPackTransport openPort]: Output baud rate changed to %d\n", (int) cfgetospeed(&options));

    // Cause the new options to take effect immediately.
    if (tcsetattr(fileDescriptor, TCSANOW, &options) == FAIL)
    {
        NSLog(@"[LocalSerialIOPackTransport openPort]: Error setting tty attributes %s - %s(%d).\n",
            @cStrDefaultSerialPort, strerror(errno), errno);
        [self closePort];
        return NO;
    }
    
    //success!
    return YES;
}

- (void) closePort
{
    if (fileDescriptor > DEV_INVALID_DESCRIPTOR)
    {
        NSLog(@"[LocalSerialIOPackTransport dealloc] Closing serial port");
        close(fileDescriptor);
        fileDescriptor = DEV_INVALID_DESCRIPTOR;
    }
}

- (void) release
{
    NSLog(@"[LocalSerialIOPackTransport release]");
    [super release];
}

- (void) dealloc
{
    [deviceAddress release];
    [self closePort];
    [self deallocReadBuffer];
    [super dealloc];
}

- (void)allocReadBuffer
//
// Internal utility function.  You should not call this from client code.
//
{
    // Allocate readBuffer
    
    if ( readBuffer == NULL )
    {
        readBuffer = malloc(readBufferSize);
        if ( readBuffer == NULL )
            [NSException raise:LocalSerialIOPackTransport_EX_MALLOC_FAILED
                        format:LocalSerialIOPackTransport_EX_MALLOC_FAILED];
    }
}

- (void)deallocReadBuffer
{
    if ( readBuffer )
    {
        free(readBuffer);
        readBuffer = NULL;
    }
}

/*
- (void)setReadBufferSize:(unsigned int)size
//
// Change the size of the read buffer at runtime
//
{
    readBufferSize = size;
	
    [self deallocReadBuffer];
    [self allocReadBuffer];
}
*/

/** Begin IOPackTransport implementation **/

- (int)numOutputChannels
{
    if (!hasBeenSetUp) [NSException raise:LocalSerialIOPackTransport_EX_NOT_SET_UP 
                        format:LocalSerialIOPackTransport_EX_NOT_SET_UP];
    return kSketchProtocol_numSetValues;
}

- (int)numInputChannels
{
    if (!hasBeenSetUp) [NSException raise:LocalSerialIOPackTransport_EX_NOT_SET_UP 
                        format:LocalSerialIOPackTransport_EX_NOT_SET_UP];
    return kSketchProtocol_numSetValues;
}

- (BOOL)isConnectedAndReady
{
    return hasBeenSetUp && (fileDescriptor > DEV_INVALID_DESCRIPTOR) && ([self isReadable] || [self isWritable]);
}

- (BOOL)isReadable
{
    //TODO - use select() to make this more accurate
    return hasBeenSetUp && (fileDescriptor > DEV_INVALID_DESCRIPTOR);
}

- (BOOL)isWritable
{
    //TODO - use select() to make this more accurate
    return hasBeenSetUp && (fileDescriptor > DEV_INVALID_DESCRIPTOR);
}


- (int)read
//
// Read any available data and just keep it in the internal buffer
// Returns number of bytes received.  (May be 0)
//
{
    if (!hasBeenSetUp) [NSException raise:LocalSerialIOPackTransport_EX_NOT_SET_UP 
                        format:LocalSerialIOPackTransport_EX_NOT_SET_UP];

    int count;
 
    // port must be created and connected
    if ( ![self isConnectedAndReady] )
        [NSException raise:LocalSerialIOPackTransport_EX_NOT_CONNECTED 
                        format:LocalSerialIOPackTransport_EX_NOT_CONNECTED];

    // Request a read of as much as we can.  Should return immediately if no data.
    count = read(fileDescriptor, readBuffer, readBufferSize);
    
    if ( count < 0 )
    {
        // read() returned an error.
        [NSException raise:LocalSerialIOPackTransport_EX_READ_FAILED
                    format:LocalSerialIOPackTransport_EX_READ_FAILED];
    }
    
    return count;
}

- (char *)readBuffer {
    if (!hasBeenSetUp) [NSException raise:LocalSerialIOPackTransport_EX_NOT_SET_UP 
                        format:LocalSerialIOPackTransport_EX_NOT_SET_UP];
    return readBuffer;
}

- (void)writeData:(NSData*)data
{
    if (!hasBeenSetUp) [NSException raise:LocalSerialIOPackTransport_EX_NOT_SET_UP 
                        format:LocalSerialIOPackTransport_EX_NOT_SET_UP];

    const char* bytes = [data bytes];    
    int len = [data length];
    int sent;
    
    // port must be created and connected
    if ( ![self isConnectedAndReady] )
        [NSException raise:LocalSerialIOPackTransport_EX_NOT_CONNECTED 
                        format:LocalSerialIOPackTransport_EX_NOT_CONNECTED];
    
    // Send the data
    while ( len > 0 )
    {
		sent = write(fileDescriptor, bytes, len);
        
        if ( sent < 0 )
			[NSException raise:LocalSerialIOPackTransport_EX_WRITE_FAILED 
                            format:LocalSerialIOPackTransport_EX_WRITE_FAILED_F, strerror(errno)];    
        
        bytes += sent;
        len -= sent;
    }
}

/** End IOPackTransport implementation **/


@end
